﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using IronPython.Hosting;
using IronPython.Runtime;
using Microsoft.Scripting.Hosting;

namespace ACSR.PythonScripting
{

    //public delegate void MessageEvent(object sender, string Message);
    // Use the buffer
    public delegate void ControllerMessageEvent(object sender, MessageArgs args);
    public class ScriptController
    {

        public event DataEvent OnWriteBuffer;
        //public event MessageEvent OnMessage;
        public event ControllerMessageEvent OnMessage;

        private ScriptRuntimeSetup _setup;
        private ScriptRuntime _runtime;
        private ScriptEngine _engine;
        protected ScriptScope _scope;
        public ScriptScope Scope
        {
            get
            {
                return _scope;
            }
        }
        public ScriptEngine Engine
        {
            get
            {
                return _engine;
            }
        }

        private BufferedStream _outputStream;
        private BufferedStream _errorOutput;

        public ScriptController()
            : this(false)
        {
        }

        public BufferedStream Output
        {
            get
            {
                return _outputStream;
            }
        }

        public BufferedStream ErrorOutput
        {
            get
            {
                return _errorOutput;
            }
        }

        public ScriptController(bool ADebug)
        {
            // _SearchPaths = new List<string>();

            _setup = new ScriptRuntimeSetup();
            _setup.DebugMode = true;
            _setup.LanguageSetups.Add(Python.CreateLanguageSetup(null));
            _setup.DebugMode = ADebug;

            _runtime = new ScriptRuntime(_setup);
            _engine = _runtime.GetEngineByTypeName(typeof(PythonContext).AssemblyQualifiedName);

            _errorOutput = new BufferedStream(256, 100);
            _errorOutput.OnWrite += new DataEvent(_outputStream_OnWrite);
            _errorOutput.OnMessage += new MessageEvent(_errorOutput_OnMessage);

            _outputStream = new BufferedStream(4096, 100);
            _outputStream.OnMessage += new MessageEvent(_outputStream_OnMessage);
            _outputStream.OnWrite += new DataEvent(_outputStream_OnWrite);

            _engine.Runtime.IO.SetOutput(_outputStream, ASCIIEncoding.ASCII);
            _engine.Runtime.IO.SetErrorOutput(_errorOutput, ASCIIEncoding.ASCII);

            var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            SetSearchPath(assemblyDir);
            SetSearchPath(Path.Combine(assemblyDir, "Lib"));
            SetSearchPath(Path.Combine(assemblyDir, "Scripts"));
        }


        void _outputStream_OnWrite(object sender, DataEventArgs args)
        {
            if (OnWriteBuffer != null)
            {
                OnWriteBuffer(sender, args);
            }
        }
        void RaiseMessageEvent(string Message, ScriptMessageType messageType)
        {
            if (OnMessage != null)
            {
                OnMessage(this, new MessageArgs(Message, messageType));
            }

        }
        void _errorOutput_OnMessage(object sender, string Message)
        {
            RaiseMessageEvent(Message, ScriptMessageType.Error);
        }

        void _outputStream_OnMessage(object sender, string Message)
        {
            RaiseMessageEvent(Message, ScriptMessageType.Message);
        }



        public void SetSearchPath(string path)
        {
            var sp = _engine.GetSearchPaths();
            foreach (var p in sp)
            {
                if (string.Compare(p, path, false) == 0)
                {
                    return;
                }
            }
            sp.Add(path);
            _engine.SetSearchPaths(sp);
        }

        public void SetSysArgV<T>(IEnumerable<T> args)
        {
            var a = new List<T>();
            foreach (var arg in args)
            {
                a.Add(arg);
            }
            _engine.GetSysModule().SetVariable("argv", a);


        }
        ScriptSource getScriptSourceFromString(string Script)
        {
            _engine.SetSearchPaths(SearchPaths);
            return _engine.CreateScriptSourceFromString(Script);
        }
        void FlushBuffers()
        {
            _outputStream.FlushBuffer();
            _errorOutput.FlushBuffer();
        }
        public IScriptContext CreateScriptContext()
        {
            var ctx = new ScriptContext(_engine.CreateScope(), null,
                _outputStream, _engine, _runtime, () => FlushBuffers());
            return ctx;
        }

        public IScriptContext CreateScriptContextFromString(string Script)
        {
            var script = getScriptSourceFromString(Script);
            var ctx = CreateScriptContext();
            ctx.Script = script;
            return CreateScriptContextFromString(Script, ctx);
        }

        public IScriptContext CreateScriptContextFromString(string script, IScriptContext existingContext)
        {

            if (existingContext == null)
            {
                return CreateScriptContextFromString(script);
            }
            var returnScript = getScriptSourceFromString(script);
            existingContext.Script = returnScript;
            return existingContext;
            //return new ScriptContext(Ctx.GetScriptScope(), script, _outputStream);
        }

        public IScriptContext CreateScriptContextFromFile(string FileName)
        {
            // we could use usefile here.
            _engine.SetSearchPaths(SearchPaths);
            var script = _engine.CreateScriptSourceFromFile(FileName);
            var scope = _engine.CreateScope();
            return new ScriptContext(scope, script, _outputStream, _engine, _runtime, () => FlushBuffers());
        }

        /*   void bufOut_OnFlushBuffer(byte[] Data)
            {
                if (Data.Length == 0)
                    return;
                if (OnMessage != null)
                    OnMessage(this, ASCIIEncoding.ASCII.GetString(Data));
            }*/

        //private ICollection<string> _SearchPaths;
        public ICollection<string> SearchPaths
        {
            get
            {
                return _engine.GetSearchPaths();
            }
            set
            {
                _engine.SetSearchPaths(value);
            }
        }

    }

    public class LocalisedScope : ACSR.PythonScripting.ILocalisedScope
    {
        ScriptContext _scriptContext;
        ScriptScope _scope;
        public LocalisedScope(ScriptContext scriptContext, ScriptScope scope)
        {
            _scriptContext = scriptContext;
            _scope = scope;
        }
        public ILocalisedScope SetVariable(string name, object value)
        {
            _scope.SetVariable(name, value);
            return this;
        }
        public dynamic Evaluate(string expression)
        {
            return _scriptContext._engine.Execute(expression, _scope);
        }
    }

    public class ScriptContext : IScriptContext
    {
        private ScriptScope _scope;
        private ScriptSource _script;
        internal ScriptEngine _engine;
        private ScriptRuntime _runtime;
        private Action _flushBufferCallback;

        public void ImportType(Type type)
        {
            var assembly = Assembly.GetAssembly(type);

            _runtime.LoadAssembly(assembly);
            var code = string.Format("from {0} import *\nw = WTF()", type.Namespace);
            _engine.Execute(code, _scope);
        }

        public ILocalisedScope CreateLocalScope()
        {
            return new LocalisedScope(this, _engine.CreateScope());
        }

        public dynamic GetGlobals()
        {
            var tempScope = CreateLocalScope();
            tempScope.SetVariable("g", _engine.Execute("globals().items()", _scope));
            return tempScope.Evaluate("g.sort()\ng");
        }
        public dynamic FilterBuiltins(object list)
        {
            var tempScope = CreateLocalScope();
            tempScope.SetVariable("list", list);
            return tempScope.Evaluate("filter(lambda item: item[0] <> '_', list)");
        }
        public dynamic FilterNulls(object container, object memberList)
        {
            var tempScope = CreateLocalScope();
            tempScope.SetVariable("list", memberList);
            tempScope.SetVariable("container", container);
            return tempScope.Evaluate("filter(lambda item: hasattr(container, item) and getattr(container, item), list)");
        }

        public dynamic GetObjectMembers(object objectToInspect)
        {
            /*SetVariable("targetObject", objectToInspect);
            return ExecuteString("l = dir(targetObject); l.sort();\nl");*/

            var tempScope = CreateLocalScope();
            tempScope.SetVariable("objectToInspect", objectToInspect);
            //tempScope.SetVariable("globals", GetGlobals());
            return tempScope.Evaluate("def i(obj): l = dir(obj); l.sort(); return l;\ni(objectToInspect)");
        }

        public void SetGlobals(object Globals)
        {
            Python.GetBuiltinModule(_engine).SetVariable("globals", Globals);
        }



        public dynamic ExecuteString(string script)
        {
            return _engine.Execute(script, _scope);
        }

        public dynamic Execute()
        {

            try
            {
                var result = _script.Execute(_scope);
                FlushBuffer();
                return result;
            }
            catch (Exception e)
            {
                ExceptionOperations eo = _engine.GetService<ExceptionOperations>();
                string error = eo.FormatException(e);
                throw new Exception(error);
            }


        }

        public void SetVariable(string Name, object Value)
        {
            _scope.SetVariable(Name, Value);
        }

        public void InjectMembers(object source)
        {
            foreach (var member in source.GetType().GetMembers())
            {

            }
        }

        public ScriptScope GetScriptScope()
        {
            return _scope;
        }


        public dynamic Scope
        {
            get
            {
                return _scope;
            }
        }
        public ScriptSource Script
        {
            get
            {
                return _script;
            }
            set
            {
                _script = value;
            }
        }

        public ScriptContext(ScriptScope Scope, ScriptSource Script,
            BufferedStream outputBuffer,
            ScriptEngine Engine,
            ScriptRuntime runtime,
            Action flushBufferCallback)
        {
            this._engine = Engine;
            this._scope = Scope;
            this._script = Script;
            this._runtime = runtime;
            this._flushBufferCallback = flushBufferCallback;

        }

        ~ScriptContext()
        {
            //_outputBuffer.FlushBuffer();
        }

        #region IScriptContext Members


        public void FlushBuffer()
        {
            this._flushBufferCallback();

        }

        #endregion
    }

    public enum ScriptMessageType
    {
        Unknown = 0,
        Message = 1,
        Warning = 2,
        Error = 3
    }

    public class MessageArgs : EventArgs
    {
        private Dictionary<string, object> _properties;
        public MessageArgs(string message, ScriptMessageType messageType)
        {
            this.Message = message;
            this.MessageType = messageType;
            _properties = new Dictionary<string, object>();
        }

        public Dictionary<string, object> Properties
        {
            get
            {
                return _properties;
            }
        }
        public string Message { get; set; }

        public ScriptMessageType MessageType { get; set; }
    }

}
